<?php
/**
 * Copyright (c) 2006-2007, Julien PORTALIER
 *
 * Licensed under The MIT License
 * Redistributions of files must retain the above copyright notice.
 *
 * @copyright    Copyright (c) 2006-2007, Julien PORTALIER
 * @link         http://featherphp.googlecode.com/
 * @license      http://www.opensource.org/licenses/mit-license.php The MIT License
 */

/**
 * MySQL backend.
 */
class db_mysql extends db_base
{
	protected $operators  = '=|<|>|<=|>=|<>|<=>|like|regexp';
	protected $fieldQuote = '`';
	public    $schema;
	
	function __construct($conf)
	{
		parent::__construct($conf);
		$this->schema = $this->conf['database'];
	}
	
	/**
	 * Connects the database.
	 */
	protected function __start()
	{
		# ignition
		$connect = "mysql_".(empty($this->conf['permanent']) ? 'connect' : 'pconnect');
		$this->link = $connect($this->conf['host'], $this->conf['login'], $this->conf['password'])
			or $this->error();
		
		# schema to use
		if (!empty($this->conf['database']))
			$this->useDatabase($this->conf['database']);
		
		# unicode?
		if ($this->conf['unicode'])
			$this->execute("SET NAMES 'UTF8' ;");
	}
	
	/**
	 * Disconnects the database.
	 */
	protected function __stop()
	{
		if ($this->link)
		{
			mysql_close($this->link);
			$this->link = false;
		}
	}
	
	/**
	 * Selects a database to use, from now on.
	 *
	 * @param $database string The database to use.
	 */
	function useDatabase($database=null)
	{
#		if (empty($database))
#			$database = $this->conf['database'];
		mysql_select_db($database, $this->link) or $this->error();
	}
	
	# SQL
	
	/**
	 * Sets transaction mode:
	 *
	 * @param $com string  'start', 'commit', 'rollback', 'autocommit'.
	 * @param $com boolean Optional parameter for 'autocommit'.
	 */
	function transactions($com)
	{
		switch($com)
		{
#			case 'start':    return $this->execute('START TRANSACTION ;');
			case 'start':    return $this->execute('BEGIN ;');
			case 'commit':   return $this->execute('COMMIT ;');
			case 'rollback': return $this->execute('ROLLBACK ;');
		}
	}
	
	/**
	 * Executes an SQL query for which no return data is awaited or
	 * no processing to the resulset is wanted.
	 * 
	 * @param  $sql string Valid SQL query.
	 * @return MySQL resultset.
	 */
	function execute($sql)
	{
		if (!$this->link)
			$this->__start();
		
		# execute & time request
		$_start = microtime(true);
		$rs     = mysql_query($sql, $this->link);
		$_end   = microtime(true);
		
		# error?
		if (!$rs)
			$this->error($sql);
		else if (DEBUG < 2)
		{
			# slow request?
			if ($_end - $_start > 1.0)
				file_log(sprintf('%.2fs : %s', $_end - $_start, $sql), 'db_mysql_slow_queries');
		}
		else
		{
			# debug
			$this->debug($sql, $_end - $_start);
		}
		
		return $rs;
	}
	
	/**
	 * Executes an SQL query, and returns data.
	 * 
	 * Data is returned in 3 possible forms, depending on $process
	 * (defaults to Q_PROCESS_FULL).
	 * 
	 * Q_PROCESS_FULL:
	 *   Useful when calling data from multiple tables at once.
	 *   Format: $data[table][field] = values
	 * 
	 * Q_PROCESS_LITE:
	 *   Useful when calling data from a single table.
	 *   Format: $data[field] = values
	 * 
	 * Q_PROCESS_NONE:
	 *   No processing is done.
	 *   Format: $data[] = values
	 * 
	 * Q_PROCESS_LIST:
	 *   Lists a single value (eg: a list of tags).
	 *   Format: $data = array(value, value, value)
	 * 
	 * Q_PROCESS_ASSOC_LIST:  
	 *   Lists a pair of values (eg: useful for HTML's select).
	 *   Format: $data = array(key => value2, key => value2)
	 * 
	 * @return array Processed resultset.
	 * @param $sql string Valid SQL query.
	 * @param $process integer Q_PROCESS_FULL|Q_PROCESS_LITE|Q_PROCESS_NONE|Q_PROCESS_LIST.
	 * @param $tableName mixed In rare cases where we can't get table's name.
	 */
	function & select($sql, $process=Q_PROCESS_FULL, $tableName=0)
	{
		# query
		if (is_array($sql))
			$sql = $this->prepareSelect(&$sql);
		$rs = $this->execute($sql);
		
		# parsing data
		$data = array();
		if ($rs and mysql_num_rows($rs) > 0)
		{
			switch($process)
			{
				# data[table][field] = value
				case Q_PROCESS_FULL:
					while ($row = mysql_fetch_row($rs))
					{
						$r = array();
						foreach ($row as $idx => $v)
						{
							$table = mysql_field_table($rs, $idx);
							if (empty($table))
								$table = $tableName;
							$r[$table][mysql_field_name($rs, $idx)] = $v;
						}
						array_push($data, $r);
					}
					break;
				
				# data[field] = value
				case Q_PROCESS_LITE:
					while ($row = mysql_fetch_assoc($rs))
						array_push($data, $row);
					break;
				
				# data[index] = value
				case Q_PROCESS_LIST:
					while ($row = mysql_fetch_row($rs))
						array_push($data, $row[0]);
					break;
				
				# data[key] = value
				case Q_PROCESS_ASSOC_LIST:
					while ($row = mysql_fetch_row($rs))
						$data[$row[0]] = $row[1];
					break;
				
				# value
				case Q_PROCESS_VALUE:
					$row = mysql_fetch_row($rs);
					return $row[0];
				
				# data[] = value
				default:
					while ($row = mysql_fetch_row($rs))
						array_push($data, $row);
			}
			
			# cleanup
			mysql_free_result($rs);
		}
		return $data;
	}
	
	/**
	 * Analyzes a table.
	 *
	 * @return array Table's description. 
	 * @param $table string The table to analyze. 
	 */
	function & describeTable($table)
	{
		$fields = array();
		$rs = $this->execute("DESC $table ;");
		
		while ($f = mysql_fetch_row($rs))
		{
			$name = array_shift($f);
			$type = array_shift($f);
			
			$fields[$name] = array(
				'type' => $type,
				'null' => (array_shift($f) != 'NO'),
				'key'  => (array_shift($f) == 'PRI') ? true  : false,
			);
			
			if ($type == 'tinytext')
				$fields[$name]['length'] = 255;
			elseif (strpos($type, 'varchar(') === 0)
				$fields[$name]['length'] = str_replace(array('varchar(', ')'), '', $type);
		}
		
		mysql_free_result($rs);
		return $fields;
	}
	
	// data
	
	/**
	 * Sets a limit (with pagination).
	 * 
	 * @return string SQL ready limiter.
	 * @param $limit int Number of elements to display.
	 * @param $page  int For pagination.
	 */
	function limit($limit, $page=1)
	{
		if ($page > 1)
			return "LIMIT ".(($page - 1) * $limit).", ".$limit;
		elseif ($limit)
			return "LIMIT $limit";
	}
	
	/**
	 * Quotes a single field.
	 */
	/*
	function field($f)
	{
		// do not process field?
		if (strpos($f, '-! ') === 0)
			return str_replace('-! ', '', $f);
		
		// operator
		if (stripos($f, 'DISTINCT') !== false)
		{
			$op = 'DISTINCT';
			$f = str_ireplace('DISTINCT', '', $f);
		}
		else {
			$op = '';
		}
		$f = trim($f);
		
		if (strpos($f, '.'))
		{
			// model.field type
			list($m, $f) = explode('.', $f);
			$f = ($f != '*') ? "`$m`.`$f`" : "`$m`.$f";
		}
		else
		{
			// simple field
			$f = ($f != '*') ? "`$f`" : $f;
		}
		
		return "$op $f";
	}
	*/
	/**
	 * Quotes a single value, depending on it's type.
	 */
	function value($v)
	{
		if (!$this->link)
			$this->__start();
		
		if (strpos($v, '-! ') === 0)
		{
			# do not process value
			return str_replace('-! ', '', $v);
		}
		elseif ($v === true)
			return 1;
		elseif ($v === false)
			return 0;
#		elseif (!is_numeric($v))
#		{
			# null
			if (empty($v) and strlen($v) == 0)
				return 'NULL';
			
			# string or numeric
#			$v = str_replace(CRLF, LF, $v);
			$v = mysql_real_escape_string($v, $this->link);
			return "'$v'";
#		}
#		return $v;
	}
	
	/**
	 * Returns the last created ID.
	 */
	function getLastInsertId()
	{
		return mysql_insert_id();
	}
	
	// debug
	
	/**
	 * Reports an error. Directly if DEBUG, logs it otherwise. 
	 */
	protected function error($sql=null)
	{
		if (DEBUG)
		{
			if (!is_null($sql))
				echo '<p class="monospace">'.$sql.'</p>';
			die("MySQL error ".mysql_errno().": ".mysql_error());
		}
		
		# log
		file_log(mysql_errno().": ".mysql_error(), 'db_mysql_errors');
		Error::show(500);
	}
	
	/**
	 * Records the query (when in SQL DEBUG mode).
	 */
	protected function debug($sql, $time=null)
	{
		$q = array(
			'sql'      => $sql,
			'error'    => mysql_errno($this->link)." ".mysql_error($this->link),
			'affected' => mysql_affected_rows($this->link),
			'time'     => $time
		);
		array_push($this->debugs, $q);
	}
}
?>